package com.dtflys.easyel.runtime;

import com.dtflys.easyel.exception.EasyElAccessException;
import com.dtflys.easyel.exception.EasyElAmbiguousMethodsException;
import com.dtflys.easyel.exception.EasyElConstructorException;
import com.dtflys.easyel.utils.AccessUtils;
import com.dtflys.easyel.utils.MetaHelper;

import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

/**
 * @author gongjun[jun.gong@thebeastshop.com]
 * @since v1.0.0
 */
public abstract class MetaClass {

    private final Class<?> clazz;

    private boolean isInitialized = false;

    private final Map<String, Field> publicFields = new HashMap<>();

    private final Map<String, MetaProperty> properties = new HashMap<>();

    private final SameNameMethodCollection constructors = new SameNameMethodCollection("<INIT>");

    private final Map<String, MetaMethod> nonParamsMethods = new HashMap<>();

    private final Map<String, SameNameMethodCollection> methods = new HashMap<>();

    private final Map<String, MetaMethod> staticNonParamsMethods = new HashMap<>();

    private final Map<String, SameNameMethodCollection> staticMethods = new HashMap<>();

    private final Map<String, Field> staticPublicFields = new HashMap<>();

    private final Map<Class, Integer> typeDistanceMap = new HashMap<>();


    public MetaClass(Class<?> clazz) {
        this.clazz = clazz;
    }

    public boolean isInitialized() {
        return isInitialized;
    }

    public void initialize() {
        if (!isInitialized) {
            initializePublicFields();
            initializeConstructors();
            initializePropertiesAndMethods();
//            initializeTypeDistance(this.typeDistanceMap);
            isInitialized = true;
        }
    }

//    protected abstract void initializeTypeDistance(Map<Class, Integer> typeDistanceMap);



    private void initializeConstructors() {
        Constructor[] ctrs = clazz.getConstructors();
        for (Constructor ctr : ctrs) {
            MetaConstructor metaConstructor = new MetaConstructor(ctr);
            this.constructors.addMethod(metaConstructor);
        }
    }

    private void initializePublicFields() {
        if (clazz.isPrimitive()) {
            return;
        }
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            int modifiers = field.getModifiers();
            if (Modifier.isPublic(modifiers)) {
                if (Modifier.isStatic(modifiers)) {
                    staticPublicFields.put(field.getName(), field);
                } else {
                    publicFields.put(field.getName(), field);
                }
            }
        }
    }

    protected void initializePropertiesAndMethods() {
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            String name = method.getName();
            String keyName = name;
            Class<?>[] paramTypes = method.getParameterTypes();
            boolean isVarArgs = method.isVarArgs();
            int modifiers = method.getModifiers();
            if (Modifier.isStatic(modifiers)) {
                if (paramTypes.length == 0 && !isVarArgs) {
                    MetaMethod staticNonParamsMethod = new InvokableMetaMethod(method);
                    staticNonParamsMethods.put(name, staticNonParamsMethod);
                } else {
                    SameNameMethodCollection sameNameMethodCollection = this.staticMethods.get(keyName);
                    if (sameNameMethodCollection == null) {
                        sameNameMethodCollection = new SameNameMethodCollection(name);
                        staticMethods.put(keyName, sameNameMethodCollection);
                    }
                    MetaMethod metaMethod = new InvokableMetaMethod(method);
                    sameNameMethodCollection.addMethod(metaMethod);
                }
            } else if (paramTypes.length == 0 && !isVarArgs) {
                if (AccessUtils.isGetterName(name)) {
                    String propName = AccessUtils.getPropertyName(name);
                    if (propName == null) {
                        throw new RuntimeException("'" + name + "' can not be a Property");
                    }
                    MetaProperty property = properties.get(propName);
                    if (property == null) {
                        property = new MetaProperty(propName);
                        properties.put(propName, property);
                    }
                    property.setGetterMethod(method);
                }
                MetaMethod nonParamsMethod = new InvokableMetaMethod(method);
                nonParamsMethods.put(name, nonParamsMethod);
            } else if (paramTypes.length == 1
                    && !isVarArgs
                    && AccessUtils.isSetterName(name)) {
                String propName = AccessUtils.getPropertyName(name);
                if (propName == null) {
                    throw new RuntimeException("'" + name + "' can not be a Property");
                }
                MetaProperty property = properties.get(propName);
                if (property == null) {
                    property = new MetaProperty(propName);
                    properties.put(propName, property);
                }
                property.setSetterMethod(method);
            } else {
                SameNameMethodCollection sameNameMethodCollection = this.methods.get(keyName);
                if (sameNameMethodCollection == null) {
                    sameNameMethodCollection = new SameNameMethodCollection(name);
                    this.methods.put(keyName, sameNameMethodCollection);
                }

                MetaMethod metaMethod = sameNameMethodCollection.getSameParamTypesMethod(paramTypes);
                if (metaMethod == null) {
                    metaMethod = new InvokableMetaMethod(method);
                    sameNameMethodCollection.addMethod(metaMethod);
                }
            }
        }
    }


    public void setTypeDistance(Class type, int distance) {
        this.typeDistanceMap.put(type, distance);
    }


    public MetaMethod getMethod(String methodName, Object[] arguments) throws EasyElAmbiguousMethodsException {
        if (arguments.length == 0) {
            return nonParamsMethods.get(methodName);
        }
        SameNameMethodCollection sameNameMethodCollection = methods.get(methodName);
        if (sameNameMethodCollection == null) return null;
        return sameNameMethodCollection.chooseMethod(clazz, arguments);
    }


    public MetaMethod getStaticMethod(String methodName, Object[] arguments) throws EasyElAmbiguousMethodsException {
        if (arguments.length == 0) {
            return staticNonParamsMethods.get(methodName);
        }
        SameNameMethodCollection sameNameMethodCollection = staticMethods.get(methodName);
        if (sameNameMethodCollection == null) return null;
        return sameNameMethodCollection.chooseMethod(clazz, arguments);
    }


    public Field getField(String fieldName) {
        return publicFields.get(fieldName);
    }

    public Field getStaticField(String fieldName) {
        return staticPublicFields.get(fieldName);
    }

    public MetaProperty getProperty(String propertyName) {
        return properties.get(propertyName);
    }

    public Object access(Object self, String name) throws InvocationTargetException, IllegalAccessException, EasyElAccessException {
        InvokableMetaMethod metaMethod = (InvokableMetaMethod) nonParamsMethods.get(name);
        if (metaMethod != null) {
            return metaMethod.invokeWithEmptyArguments(self);
        }
        MetaProperty metaProperty = getProperty(name);
        if (metaProperty != null && metaProperty.isReadable()) {
            return metaProperty.getValue(self);
        }
        Field field = getField(name);
        if (field != null) {
            return field.get(self);
        }
        throw new EasyElAccessException(clazz, name);
    }


    public void inject(Object self, String name, Object value) throws InvocationTargetException, IllegalAccessException, EasyElAccessException {
        MetaProperty metaProperty = getProperty(name);
        if (metaProperty != null && metaProperty.isWritable()) {
            metaProperty.setValue(self, value);
            return;
        }
        Field field = getField(name);
        if (field != null) {
            field.set(self, value);
            return;
        }
        throw new EasyElAccessException(clazz, name);
    }


    public Class<?> getClazz() {
        return clazz;
    }


    public Object newInstance(Object[] arguments) throws EasyElAmbiguousMethodsException, EasyElConstructorException, IllegalAccessException, InstantiationException, InvocationTargetException {
        Object[] unwrappedArgs = MetaHelper.unwrap(arguments);
        MetaConstructor metaConstructor = (MetaConstructor) constructors.chooseMethod(clazz, unwrappedArgs);
        if (metaConstructor == null) {
            throw new EasyElConstructorException(clazz, unwrappedArgs);
        }
        return metaConstructor.newInstance(unwrappedArgs);
    }


    public Object invokeMethod(Object self, String methodName, Object[] arguments) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, EasyElAmbiguousMethodsException {
        Object[] unwrappedArgs = MetaHelper.unwrap(arguments);
        InvokableMetaMethod metaMethod = (InvokableMetaMethod) getMethod(methodName, unwrappedArgs);
        if (metaMethod == null) {
            String msg = clazz.getName() + "." + methodName + MetaHelper.toArgumentTypesString(arguments);
            throw new NoSuchMethodException(msg);
        }
        return metaMethod.invoke(self, unwrappedArgs);
    }


    public Object invokeStaticMethod(String methodName, Object[] arguments) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, EasyElAmbiguousMethodsException {
        Object[] unwrappedArgs = MetaHelper.unwrap(arguments);
        InvokableMetaMethod metaMethod = (InvokableMetaMethod) getStaticMethod(methodName, unwrappedArgs);
        if (metaMethod == null) {
            String msg = clazz.getName() + "." + methodName + MetaHelper.toArgumentTypesString(arguments);
            throw new NoSuchMethodException(msg);
        }
        return metaMethod.invoke(null, unwrappedArgs);
    }


    public long getTypeDistance(Class type) {
        Integer distance = typeDistanceMap.get(type);
        if (distance == null) {
            distance = MetaHelper.calculateTypeDistance(clazz, type);
            synchronized (typeDistanceMap) {
                if (!typeDistanceMap.containsKey(type)) {
                    typeDistanceMap.put(type, distance);
                }
            }
        }
        return distance;
    }

}
